﻿/* Open source distributed without warranties of any kind. */

namespace Transcriber.Plugins
{
    #region Using
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Windows.Forms;
    using global::Transcriber.Plugins.Properties;
    using Transcriber.Api;
    #endregion

    /// <summary>
    /// <see cref="ITranscriberPlugin"/> that uses an XML-based
    /// rules file to control transcription.
    /// </summary>
    public sealed class RulesFileTranscriber : TranscriberPlugin
    {
        /// <summary>
        /// Helper class used during transcription.
        /// </summary>
        /// <remarks>
        /// As each rule is processed, the source text is divided
        /// into regions that match the rule's pattern from those
        /// regions that do not. Each region is added to a list
        /// of <see cref="TranscriptionElement"/> objects. Nodes
        /// representing previously matched and replaced text are
        /// marked with <see cref="TranscriptionElement.mAlreadyTransformed"/>
        /// set to <c>true</c>, while unmatched text is marked with
        /// <see cref="TranscriptionElement.mAlreadyTransformed"/>
        /// set to <c>false</c>. Subsequent rules process those
        /// <see cref="TranscriptionElement.mText"/> strings whose
        /// <see cref="TranscriptionElement.mAlreadyTransformed"/>
        /// are <c>false</c>, further subdividing the previously
        /// transformed regions. After all rules have been processed,
        /// the final output is generated by concatenating the final
        /// list of <see cref="TranscriptionElement.mText"/> strings,
        /// using the value of the <see cref="Rules.UnmatchedAction"/>
        /// enumeration to decide what to do with any remaining
        /// unmatched nodes.
        /// </remarks>
        private sealed class TranscriptionElement
        {
            /// <summary>
            /// The region of output text represented by this node.
            /// </summary>
            private string mText;

            /// <summary>
            /// Value indicating whether or not <see cref="mText"/>
            /// represents replacement text from some rule (<c>true</c>)
            /// or source text unmatched by any rule that has yet
            /// been processed (<c>false</c>).
            /// </summary>
            private bool mAlreadyTransformed;

            /// <summary>
            /// Gets or sets the text.
            /// </summary>
            /// <value>The text.</value>
            public string Text
            {
                get { return mText; }
                set { mText = value; }
            }

            /// <summary>
            /// Gets a value indicating whether this node represents
            /// a region of text that has already been transformed.
            /// </summary>
            /// <value><c>true</c> if already transformed; otherwise, <c>false</c>.</value>
            public bool AlreadyTransformed
            {
                get { return mAlreadyTransformed; }
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="TranscriptionElement"/> class.
            /// </summary>
            /// <param name="text">The text.</param>
            /// <param name="alreadyTransformed">if set to <c>true</c> already transformed.</param>
            public TranscriptionElement(string text, bool alreadyTransformed)
            {
                this.mText = text;
                this.mAlreadyTransformed = alreadyTransformed;
            }
        }

        /// <summary>
        /// The currently loaded <see cref="Rules"/>
        /// </summary>
        private Rules mRules;

        /// <summary>
        /// The name of the currently loaded <see cref="Rules"/>
        /// </summary>
        private string mCurrentRules;

        /// <summary>
        /// Gets the name of this plug-in object.
        /// </summary>
        /// <value>The name.</value>
        public override string Name
        {
            get { return "Rules File Transcriber"; }
        }

        /// <summary>
        /// Gets or sets the current rules.
        /// </summary>
        /// <value>The current rules.</value>
        public string CurrentRules
        {
            get { return this.mCurrentRules; }
            set { this.LoadRules(value); }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RulesFileTranscriber"/> class.
        /// </summary>
        /// <param name="transcriber">The transcriber.</param>
        public RulesFileTranscriber(ITranscriberApplication transcriber)
            : base(transcriber)
        {
            string rules = Settings.Default.RulesFile;

            if (null == rules)
            {
                foreach (FileInfo file in Rules.RulesDirectory.GetFiles("*.xml"))
                {
                    rules = file.Name;
                    break;
                }
            }

            if (null != rules)
            {
                LoadRules(rules);
            }
        }

        /// <summary>
        /// Gets the settings dialog.
        /// </summary>
        /// <returns>The settings dialog.</returns>
        public override Form GetSettingsDialog()
        {
            return new frmRulesFileTranscriberSettings(this);
        }

        /// <summary>
        /// Transforms the given <paramref name="text"/>.
        /// </summary>
        /// <param name="text">The source text.</param>
        /// <returns>The transformed text.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public override string Transcribe(string text)
        {
            if (null == text)
            {
                throw new ArgumentNullException("text");
            }

            if (null == this.mRules)
            {
                throw new InvalidOperationException("No rules file loaded.");
            }

            // Compile each regex for speed and assume multiline input text.
            RegexOptions options = RegexOptions.Compiled | RegexOptions.Multiline;

            // Ignore case if instructed to do so by the rules file.
            if (!this.mRules.CaseSensitive)
            {
                options |= RegexOptions.IgnoreCase;
            }

            // The initial transcription node list consists of the entire untransformed source string.
            List<TranscriptionElement> transcription = new List<TranscriptionElement>();
            transcription.Add(new TranscriptionElement(text, false));

            // Process all rules, in sequence...
            foreach (RulesElement rulesElement in this.mRules.Items)
            {
                try
                {
                    Rule rule = rulesElement as Rule;

                    if (null == rule)
                    {
                        throw new InvalidOperationException("Unsupported rules element " + rulesElement);
                    }

                    // Compile the current rule's regex.
                    Regex regex = new Regex(rule.Pattern, options);

                    // Begin building the new transcription node list based on the current rule.
                    List<TranscriptionElement> newTranscription = new List<TranscriptionElement>();
                    TranscriptionElement newNode = null;

                    // Apply the current rule to each transcription node in the current list...
                    while (transcription.Count > 0)
                    {
                        TranscriptionElement oldElement = transcription[0];
                        transcription.RemoveAt(0);

                        // Pass through already transformed nodes to the new list.
                        if (oldElement.AlreadyTransformed)
                        {
                            newNode = this.GetTranscriptionElement(newTranscription, newNode, oldElement.Text, oldElement.AlreadyTransformed);
                            continue;
                        }

                        // Apply the current rule to the text in the current untransformed node...

                        // Start at the beginning of the current node's text.
                        int start = 0;

                        // For each matching region in the current node...
                        foreach (Match match in regex.Matches(oldElement.Text))
                        {
                            // Add a new node for any unmatched text preceding the current match.
                            if (start < match.Index)
                            {
                                newNode = this.GetTranscriptionElement(newTranscription, newNode, oldElement.Text.Substring(start, match.Index - start), false);
                            }

                            // Set the start index for the next replacement to the end of the current one.
                            start = match.Index + match.Length;
                            // Add the replacement text from the current rule to the node list.
                            foreach (OutputNode outputNode in rule.Nodes)
                            {
                                IRuleEvaluator evaluator = null;

                                if (!String.IsNullOrEmpty(outputNode.Evaluator))
                                {
                                    Type type = Type.GetType(outputNode.Evaluator, true);
                                    evaluator = (IRuleEvaluator)type.GetConstructor(Type.EmptyTypes).Invoke(null);
                                }

                                string replacement = String.IsNullOrEmpty(outputNode.Text) ? String.Empty : outputNode.Text;
                                replacement = (null == evaluator) ? regex.Replace(match.Value, replacement) : regex.Replace(match.Value, new MatchEvaluator(evaluator.Evaluate));
                                newNode = this.GetTranscriptionElement(newTranscription, newNode, replacement, !outputNode.SearchContent);
                            }
                        }

                        // Add a new node for any unmatched text following the last match.
                        if (start < oldElement.Text.Length)
                        {
                            newNode = this.GetTranscriptionElement(newTranscription, newNode, oldElement.Text.Substring(start), false);
                        }
                    }

                    // Set the current node list to the new one just generated.
                    transcription = newTranscription;
                }
                catch
                {
                    // TODO: log errors...
                }
            }

            // Generate the output...

            StringBuilder builder = new StringBuilder();

            // For each node in the transcription list...
            while (transcription.Count > 0)
            {
                TranscriptionElement element = transcription[0];
                transcription.RemoveAt(0);

                // Append the node's text if it is the output of some rule.
                if (element.AlreadyTransformed)
                {
                    builder.Append(element.Text);
                }
                else
                {
                    // Apply the "unmatched action" specified for the current set of rules...
                    switch (this.mRules.UnmatchedAction)
                    {
                        case UnmatchedAction.EmitOriginal:
                            builder.Append(element.Text);
                            break;

                        case UnmatchedAction.EmitReplacement:
                            builder.Append(this.mRules.ReplacementText);
                            break;

                        case UnmatchedAction.Omit:
                            // Just skip this node.
                            break;

                        default:
                            // TODO: log errors
                            break;
                    }
                }
            }

            return builder.ToString();
        }

        /// <summary>
        /// Modifies or adds a transcription element.
        /// </summary>
        /// <remarks>
        /// If <paramref name="alreadyTransformed"/> has the same value as
        /// the <see cref="TranscriptionElement.AlreadyTransformed"/>
        /// then append <paramref name="text"/> to the
        /// <see cref="TranscriptionElement.Text"/> of the given
        /// <paramref name="oldElement"/> and return it. Otherwise,
        /// create a new <see cref="TranscriptionElement"/>,
        /// add it to <paramref name="transcription"/> and
        /// return it.
        /// </remarks>
        /// <param name="transcription">The current transcription.</param>
        /// <param name="oldElement">The old element.</param>
        /// <param name="text">The text.</param>
        /// <param name="alreadyTransformed">if set to <c>true</c> [already transformed].</param>
        /// <returns>The new or modified <see cref="TranscriptionElement"/></returns>
        private TranscriptionElement GetTranscriptionElement(List<TranscriptionElement> transcription, TranscriptionElement oldElement, string text, Boolean alreadyTransformed)
        {
            if ((null != oldElement) && (alreadyTransformed == oldElement.AlreadyTransformed))
            {
                oldElement.Text += text;
                return oldElement;
            }
            else
            {
                TranscriptionElement newElement = new TranscriptionElement(text, alreadyTransformed);
                transcription.Add(newElement);
                return newElement;
            }
        }

        /// <summary>
        /// Loads the rules.
        /// </summary>
        /// <param name="currentRules">The current rules.</param>
        private void LoadRules(string currentRules)
        {
            if (null != currentRules)
            {
                try
                {
                    FileInfo file = new FileInfo(Path.Combine(Rules.RulesDirectory.FullName, currentRules));
                    this.mRules = Rules.Load(file);
                    this.mCurrentRules = currentRules;
                    Settings.Default.RulesFile = currentRules;
                }
                catch
                {
                    MessageBoxOptions mbOptions = MessageBoxOptions.DefaultDesktopOnly;

                    if ((null != Form.ActiveForm) &&  (RightToLeft.Yes == Form.ActiveForm.RightToLeft))
                    {
                        mbOptions |= (MessageBoxOptions.RightAlign | MessageBoxOptions.RtlReading);
                    }

                    this.mRules = null;
                    MessageBox.Show(string.Format("Error loading {0}", currentRules), "Error Loading Rules", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, mbOptions);
                }
            }
        }

        /// <summary>
        /// Gets the source context menu items.
        /// </summary>
        /// <returns>The source context menu items.</returns>
        public override ToolStripItem[] GetSourceContextMenuItems()
        {
            ToolStripItem convertNumberItem = new ToolStripButton("Convert Number...", null, new EventHandler(this.OnConvertNumber));
            ToolStripItem testRegexItem = new ToolStripButton("Test Regex...", null, new EventHandler(this.OnTestRegex));
            return new ToolStripItem[] { convertNumberItem, testRegexItem };
        }

        /// <summary>
        /// Terminates this instance.
        /// </summary>
        protected override void Terminate()
        {
            try
            {
                Settings.Default.Save();
            }
            catch
            {
            }
            base.Terminate();
        }

        private void OnTestRegex(object sender, EventArgs e)
        {
            using (frmRegexTest dialog = new frmRegexTest())
            {
                dialog.ShowDialog();
            }
        }

        private void OnConvertNumber(object sender, EventArgs e)
        {
            using (frmNumericBaseConverter dialog = new frmNumericBaseConverter())
            {
                dialog.ShowDialog();
            }
        }
    }
}